﻿#include "pch.h"
#if _SB_WINDOWS_ && !_QG_USE_SDL
#include "qgp/qgach.h"
#include "extend/sbvalues.h"
#include <windowsx.h>
#include <Xinput.h>

#if !defined(_WIN32_WINNT_WIN8) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
#define PAD_NOBATTERY				1
#endif
#define PAD_DEADZONE				(0.24f*((float)0x7FFF))

//////////////////////////////////////////////////////////////////////////
// 윈도우 스텁
namespace _windows
{
	class WindowsStub;

	// IMM
	static struct ImplImm
	{
		typedef HIMC(WINAPI *stubImmAssociateContext)(IN HWND, IN HIMC);

		kModule* md;
		stubImmAssociateContext funcImmAssociateContext;

		kbool Initialize()
		{
			md = k_mod_open("imm32", NULL, 0);
			k_return_value_if_fail(md, false);

			k_mod_sym_func(md, "ImmAssociateContext", (kfunc*)&funcImmAssociateContext);

			return true;
		}

		void Dispose()
		{
			if (md)
				k_mod_close(md);
		}
	} impl_imm = {NULL, };

	// XINPUT
	static struct ImplXInput
	{
		typedef void (WINAPI* stubXInputEnable)(
			_In_ BOOL enable     // [in] Indicates whether xinput is enabled or disabled.
			);
		typedef DWORD(WINAPI* stubXInputGetState)(
			_In_  DWORD         dwUserIndex,  // Index of the gamer associated with the device
			_Out_ XINPUT_STATE* pState        // Receives the current state
			);
		typedef DWORD(WINAPI* stubXInputSetState)(
			_In_ DWORD             dwUserIndex,  // Index of the gamer associated with the device
			_In_ XINPUT_VIBRATION* pVibration    // The vibration information to send to the controller
			);
		typedef DWORD(WINAPI* stubXInputGetCapabilities)(
			_In_  DWORD                dwUserIndex,   // Index of the gamer associated with the device
			_In_  DWORD                dwFlags,       // Input flags that identify the device type
			_Out_ XINPUT_CAPABILITIES* pCapabilities  // Receives the capabilities
			);
#if !PAD_NOBATTERY
		typedef DWORD(WINAPI* stubXInputGetBatteryInformation)(
			_In_  DWORD                       dwUserIndex,        // Index of the gamer associated with the device
			_In_  BYTE                        devType,            // Which device on this user index
			_Out_ XINPUT_BATTERY_INFORMATION* pBatteryInformation // Contains the level and types of batteries
			);
#endif

		kModule* md;
		stubXInputEnable funcXInputEnable;
		stubXInputGetState funcXInputGetState;
		stubXInputSetState funcXInputSetState;
		stubXInputGetCapabilities funcXInputGetCapabilities;
#if !PAD_NOBATTERY
		stubXInputGetBatteryInformation funcXInputGetBatteryInformation;
#endif

		kbool Initialize()
		{
			k_return_value_if_ok(md, true);

			static char xi[64] = "xinput1_ ";

			for (kint i = 4; i >= 1; i--)
			{
				xi[8] = (char)('0' + i);

				if ((md = k_mod_open(xi, NULL, 0)) != NULL)
					break;
			}

			k_return_value_if_fail(md, false);

			k_mod_sym_func(md, "XInputEnable", (kfunc*)&funcXInputEnable);
			k_mod_sym_func(md, "XInputGetState", (kfunc*)&funcXInputGetState);
			k_mod_sym_func(md, "XInputSetState", (kfunc*)&funcXInputSetState);
			k_mod_sym_func(md, "XInputGetCapabilities", (kfunc*)&funcXInputGetCapabilities);
#if !PAD_NOBATTERY
			k_mod_sym_func(md, "XInputGetBatteryInformation", (kfunc*)&funcXInputGetBatteryInformation);
#endif

#if !PAD_NOBATTERY
			if (!funcXInputEnable ||
				!funcXInputGetState ||
				!funcXInputSetState ||
				!funcXInputGetCapabilities ||
				!funcXInputGetBatteryInformation)
#else
			if (!funcXInputEnable ||
				!funcXInputGetState ||
				!funcXInputSetState ||
				!funcXInputGetCapabilities)
#endif
			{
				k_mod_close(md);
				md = NULL;

				return false;
			}

			return true;
		}

		void Dispose()
		{
			if (md)
				k_mod_close(md);
		}
	} impl_xinput = {NULL, };

	//
	static struct ImplHook
	{
		HHOOK keyhook;
		WindowsStub* keystub;

		void Dispose()
		{
			if (keyhook)
				UnhookWindowsHookEx(keyhook);
		}

		void UnhookKey(WindowsStub* stub)
		{
			if (keyhook && keystub == stub)
			{
				UnhookWindowsHookEx(keyhook);
				keyhook = NULL;
				keystub = NULL;
			}
		}

		void HookKey(WindowsStub* stub, HINSTANCE hinst)
		{
			if (!keystub)
			{
				keyhook = SetWindowsHookEx(WH_KEYBOARD_LL, &HookKeyProc, hinst, 0);

				if (keyhook && keyhook != INVALID_HANDLE_VALUE)
					keystub = stub;
			}
		}

		static LRESULT CALLBACK HookKeyProc(int code, WPARAM wp, LPARAM lp);
	} impl_hook = {NULL, };

	//
	inline LRESULT ImplHook::HookKeyProc(int code, WPARAM wp, LPARAM lp)
	{
		if (code < 0 || code != HC_ACTION)
			return CallNextHookEx(impl_hook.keyhook, code, wp, lp);
		else
		{
			LPKBDLLHOOKSTRUCT kh = (LPKBDLLHOOKSTRUCT)lp;
			kbool b;

			switch (wp)
			{
				case WM_KEYDOWN:
				case WM_KEYUP:
					b = kh->vkCode == VK_LWIN || kh->vkCode == VK_RWIN;
					break;

				default:
					b = false;
					break;
			}

			return b ? 1 : CallNextHookEx(impl_hook.keyhook, code, wp, lp);
		}
	}


	//////////////////////////////////////////////////////////////////////////
	// 본론
	class WindowsStub : public qgStub
	{
		KCH_AUTOCLASS(WindowsStub, qgStub, "RenderStubWindows");

	private:
		HWND				_hwnd;
		HINSTANCE			_hinstance;

		DEVMODE				_dmode;

		tpl::Rect			_wndbnd;

		STICKYKEYS			_acs_stk;
		TOGGLEKEYS			_acs_tgk;
		FILTERKEYS			_acs_ftk;

		HIMC				_himc;
		kint				_imcs;

		kbool				_mhold;

		kbool				_puse;
		kbool				_pfocus;
		tpl::Point			_pdead;

		double				_active;

	private:
		static kvoid TypeInitialize(kType type)
		{
		}

		static kvoid TypeFinalize(kType type)
		{
			_windows::impl_hook.Dispose();
			_windows::impl_xinput.Dispose();
			_windows::impl_imm.Dispose();
		}

	public:
		WindowsStub()
			: super_type()
			, _pdead((kint)-PAD_DEADZONE, (kint)PAD_DEADZONE)
		{
			_active = k_timer_get_abs(_timer);
		}

		~WindowsStub()
		{
			if (_hwnd)
			{
				HoldMouse(false);

				_insts |= QGSDI_EXIT;

				if (!K_OFLAG(_insts, QGSDI_VIRTUAL))
				{
					SetWindowLongPtr(_hwnd, GWLP_USERDATA, (LONG_PTR)NULL);
					SendMessage(_hwnd, WM_CLOSE, 0, 0);
				}
			}

			if (K_OFLAG(_insts, QGSDI_ACS))
			{
				SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &_acs_stk, 0);
				SystemParametersInfo(SPI_SETTOGGLEKEYS, sizeof(TOGGLEKEYS), &_acs_tgk, 0);
				SystemParametersInfo(SPI_SETFILTERKEYS, sizeof(FILTERKEYS), &_acs_ftk, 0);

				_windows::impl_hook.UnhookKey(this);
			}

			if (_himc)
				_windows::impl_imm.funcImmAssociateContext(_hwnd, _himc);

			if (_windows::impl_xinput.md && _puse)
				_windows::impl_xinput.funcXInputEnable(FALSE);
		}

		//
		kbool Create(const tpl::Rect& area, kint flags, const char* title)
		{
			kwchar wz[260];
			GetModuleFileName(NULL, wz, 259);

#if _SB_DEBUG_
			char cname[64];
			k_getcomputername(cname, 64 - 1);
#endif

			// 타이틀
			if (title)
				_btitle = title;

			// 데스크탑 모드 저장
			memset(&_dmode, 0, sizeof(DEVMODE));
			_dmode.dmSize = sizeof(DEVMODE);
			EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &_dmode);

			// 클래스 등록
			kwchar wcls[260], wtxt[260];
			k_conv_utf8_to_utf16(wcls, 259, GetInternalClassName(), 0);
			k_conv_utf8_to_utf16(wtxt, 259, _btitle.c_str(), 0);

			WNDCLASS wc;
			memset(&wc, 0, sizeof(WNDCLASS));
			wc.hInstance = GetModuleHandle(NULL);
			wc.hIcon = ExtractIcon(wc.hInstance, wz, 0);
			wc.style = CS_HREDRAW | CS_VREDRAW;
			wc.lpfnWndProc = &StaticWndProc;
			wc.hCursor = LoadCursor(NULL, IDC_ARROW);
			wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
			wc.lpszClassName = wcls;

			if (!RegisterClass(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
			{
				kobj_tracet(KLV_ERROR, KERR_FAIL_CREATE, "window class register failed.");
				return false;
			}

			// 크기 설정
			tpl::Point ss(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
			tpl::Point ts(area.Width(), area.Height());

			kint style;
			RECT rt;

			if (K_OFLAG(flags, QGSDF_FULL))
				style = WS_POPUP;
			else
			{
				if (K_OFLAG(flags, QGSDF_NOTITLE))
				{
					style = WS_SYSMENU | WS_POPUP | WS_CLIPCHILDREN;

					if (K_OFLAG(flags, QGSDF_LAYOUT))
						style |= WS_THICKFRAME | WS_BORDER;
				}
				else
				{
					style = WS_SYSMENU | WS_TILED | WS_CAPTION | WS_MINIMIZEBOX | WS_CLIPCHILDREN;

					if (K_OFLAG(flags, QGSDF_LAYOUT))
						style |= WS_MAXIMIZEBOX | WS_THICKFRAME | WS_BORDER;
				}
			}

			if (ts.x > 128 && ts.y > 128)
				SetRect(&rt, 0, 0, ts.x, ts.y);
			else
			{
				if (K_OFLAG(flags, QGSDF_FULL))
					SetRect(&rt, 0, 0, ss.x, ss.y);
				else
				{
					if (ss.x > 1280)
					{
						// Y크기 16:10->800, 16:9->720
						SetRect(&rt, 0, 0, 1280, 720);
					}
					else
					{
						// Y크기 4:3->480, 16:10->450, 16:9->405
						SetRect(&rt, 0, 0, 720, 450);
					}
				}
			}

			// 크기 보관
			_size.Set(rt.right - rt.left, rt.bottom - rt.top);

			// 크기 조정
			AdjustWindowRect(&rt, style, FALSE);

			tpl::Point rp(0, 0);
			tpl::Point rs(rt.right - rt.left, rt.bottom - rt.top);

			if (!K_OFLAG(flags, QGSDF_FULL))
			{
				if (area.left > 0 || area.top > 0)
				{
					rp.x = area.left;
					rp.y = area.top;
				}
				else if (rs.x < ss.x && rs.y < ss.y)
				{
#if !_SB_DEBUG_
					rp.x = (ss.x - rs.x) / 2;
					rp.y = (ss.y - rs.y) / 2;
#else
					if (k_strstr(cname, "KSH") != NULL)
					{
						rp.x = 0;
						rp.y = ss.y - rs.y - 50;
					}
#endif
				}
			}

			// 윈도우 만들기
			_hwnd = CreateWindowEx(0, wcls, wtxt, style, rp.x, rp.y, rs.x, rs.y, NULL, NULL, wc.hInstance, NULL);

			if (!_hwnd)
			{
				kobj_tracet(KLV_ERROR, KERR_FAIL_CREATE, "cannot create window.");
				UnregisterClass(wcls, wc.hInstance);
				return false;
			}

			ShowWindow(_hwnd, SW_SHOWNORMAL);
			UpdateWindow(_hwnd);

			// 윈도우 스타일 변경
			if (K_OFLAG(flags, QGSDF_FULL | QGSDF_NOTITLE))
			{
				// 원래 WindowPlacePos를 써야하는데...
				SetWindowLong(_hwnd, GWL_STYLE, WS_POPUP | WS_SYSMENU | WS_VISIBLE);
			}
			else
			{
				style = GetWindowLong(_hwnd, GWL_STYLE);
				SetWindowLong(_hwnd, GWL_STYLE, style);
			}

			// 영역 저장
			GetWindowRect(_hwnd, (LPRECT)&_wndbnd);
			GetClientRect(_hwnd, (LPRECT)&_bound);
			_size.Set(_bound.Width(), _bound.Height());

			// 설정 저장
			_hinstance = wc.hInstance;

			_flags = flags;
			_insts = QGSDI_ACTIVE | QGSDI_CURSOR | QGSDI_SCRSAVE;

#if _SB_DEBUG_
			if (K_OFLAG(flags, QGSDF_AUTO) && k_strstr(cname, "KSH") != NULL)
				K_UFLAG(&_insts, QGSDI_GOPU, true);
#endif

			// 윈도우 PTR 저장
			_handle = _hwnd;
			SetWindowLongPtr(_hwnd, GWLP_USERDATA, (LONG_PTR)this);

			// 이거 필요함?
			SetSystemAware();

			// 토글키 상태 저장
			if (GetKeyState(VK_CAPITAL) & 0x1)
				K_UFLAG(&_key.state, QIS_CAPS, true);

			if (GetKeyState(VK_SCROLL) & 0x1)
				K_UFLAG(&_key.state, QIS_SCRL, true);

			if (GetKeyState(VK_NUMLOCK) & 0x1)
				K_UFLAG(&_key.state, QIS_NUM, true);

			return true;
		}

		// 가상 스텁 생성
		kbool CreateVirtual(kpointer handle)
		{
			_hwnd = reinterpret_cast<HWND>(handle);
			_hinstance = GetModuleHandle(NULL);

			GetWindowRect(_hwnd, (LPRECT)&_wndbnd);
			GetClientRect(_hwnd, (LPRECT)&_bound);
			_size.Set(_bound.Width(), _bound.Height());

			//
			_insts = QGSDI_VIRTUAL;

			//
			_handle = _hwnd;
			SetWindowLongPtr(_hwnd, GWLP_USERDATA, (LONG_PTR)this);

			//
			SetSystemAware();

			return true;
		}

		//
		static void SetSystemAware()
		{
			// SetProcessDPIAware
			if (k_sisd_ossub() >= KSIPFS_WINDOWS_VISTA)
			{
				kModule* user32 = k_mod_open("user32", NULL, 0);

				if (user32)
				{
					BOOL(WINAPI * stub_SetProcessDPIAware)(void);

					if (k_mod_sym_func(user32, "SetProcessDPIAware", (kfunc*)&stub_SetProcessDPIAware))
						stub_SetProcessDPIAware();

					k_mod_close(user32);
				}
			}

			// 타이머
			//timeBeginPeriod(1);
		}

		//
		virtual kbool _Close()
		{
			if (!K_OFLAG(_insts, QGSDI_VIRTUAL))
			{
				HoldMouse(false);
				SendMessage(_hwnd, WM_CLOSE, 0, 0);
			}
			else
			{
				_hwnd = NULL;
				_handle = NULL;
				K_UFLAG(&_insts, QGSDI_EXIT, true);
			}

			return true;
		}

		//
		virtual kbool Layout()
		{
			GetWindowRect(_hwnd, (LPRECT)&_wndbnd);
			GetClientRect(_hwnd, (LPRECT)&_bound);

			if (!K_OFLAG(_flags, QGSDF_FULL))
			{
				kint width = _bound.Width();
				kint height = _bound.Height();

				if (qgRdh::Instance != NULL)
					qgRdh::Instance->Layout(width, height);

				if (width != (kint)_size.width || height != (kint)_size.height)
				{
					_size.Set(width, height);

					if (!_sehs.empty())
					{
						kForEach(qbSevHandler* p, _sehs)
							p->OnLayout(width, height, false);
					}
				}
			}

			return true;
		}

		//
		virtual kbool Poll(kpointer)
		{
			MSG msg;

			while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
			{
				if (GetMessage(&msg, NULL, 0, 0) <= 0)
					return false;
				else
				{
					TranslateMessage(&msg);
					DispatchMessage(&msg);
				}
			}

			_polls++;

			return true;
		}

		//
		inline float ConvControllerAxis(kint v)
		{
			if (_pdead.OnPoint(v))
				return 0.0f;

			float f = (float)(v + K_MAX_SHORT) / (float)K_MAX_SHORT;

			return f - 1.0f;
		}

		//
		virtual kbool GetControllerInput(kint slot, qgUimControllerInput* inf)
		{
			k_return_value_if_fail(_puse, false);
			k_return_value_if_fail((ksize_t)slot < 4, false);
			k_return_value_if_fail(inf, false);

			XINPUT_STATE xs;
			DWORD dw = _windows::impl_xinput.funcXInputGetState(slot, &xs);
			k_return_value_if_fail(dw == ERROR_SUCCESS, false);

			inf->btn = xs.Gamepad.wButtons;

			inf->trg.uleft = xs.Gamepad.bLeftTrigger;
			inf->trg.uright = xs.Gamepad.bRightTrigger;

			inf->lthb.x = ConvControllerAxis(xs.Gamepad.sThumbLX);
			inf->lthb.y = ConvControllerAxis(xs.Gamepad.sThumbLY);
			inf->rthb.x = ConvControllerAxis(xs.Gamepad.sThumbRX);
			inf->rthb.y = ConvControllerAxis(xs.Gamepad.sThumbRY);

			return true;
		}

		//
		virtual kbool GetControllerInfo(kint slot, qgUimControllerInfo* stt)
		{
			k_return_value_if_fail(_puse, false);
			k_return_value_if_fail((ksize_t)slot < 4, false);
			k_return_value_if_fail(stt, false);

			XINPUT_CAPABILITIES xc;
#if !PAD_NOBATTERY
			XINPUT_BATTERY_INFORMATION xb;
#endif

			if (_windows::impl_xinput.funcXInputGetCapabilities(slot, XINPUT_FLAG_GAMEPAD, &xc) == ERROR_SUCCESS)
			{
				stt->flags = xc.Flags;
				stt->type = xc.Type;
				stt->extend = xc.SubType;
			}
			else
			{
				stt->flags = 0;
				stt->type = 0;
				stt->extend = 0;
			}

#if !PAD_NOBATTERY
			if (_windows::impl_xinput.funcXInputGetBatteryInformation(slot, BATTERY_DEVTYPE_GAMEPAD, &xb) == ERROR_SUCCESS)
			{
				stt->battery_type = xb.BatteryType;
				stt->battery_level = xb.BatteryLevel;
			}
			else
			{
				stt->battery_type = 0;
				stt->battery_level = 0;
			}

			if (_windows::impl_xinput.funcXInputGetBatteryInformation(slot, BATTERY_DEVTYPE_HEADSET, &xb) == ERROR_SUCCESS)
			{
				stt->headset_type = xb.BatteryType;
				stt->headset_level = xb.BatteryLevel;
			}
			else
			{
				stt->headset_type = 0;
				stt->headset_level = 0;
			}
#else
			stt->battery_type = 0;
			stt->battery_level = 0;

			stt->headset_type = 0;
			stt->headset_level = 0;
#endif

			return true;
		}

		//
		virtual kbool SetControllerEnable(kbool v)
		{
			if (_puse == v)
				return true;

			if (!_windows::impl_xinput.Initialize())
			{
				kobj_tracep(KLV_ERROR, KERR_NOSUPP);
				return false;
			}

			_windows::impl_xinput.funcXInputEnable(v);

			_puse = v;

			return true;
		}

		//
		virtual kbool SetControllerVibration(kint slot, const qgUimControllerVibration& v)
		{
			k_return_value_if_fail(_puse, false);
			k_return_value_if_fail((ksize_t)slot < 4, false);

			XINPUT_VIBRATION xc;
			xc.wLeftMotorSpeed = v.left;
			xc.wRightMotorSpeed = v.right;

			return _windows::impl_xinput.funcXInputSetState(slot, &xc) == ERROR_SUCCESS;
		}

		//
		virtual kvoid SetDropEnable(kbool v)
		{
			if (v)
			{
				if (!K_OFLAG(_insts, QGSDI_DROP))
				{
					K_UFLAG(&_insts, QGSDI_DROP, TRUE);
					DragAcceptFiles(_hwnd, TRUE);
				}
			}
			else
			{
				if (K_OFLAG(_insts, QGSDI_DROP))
				{
					K_UFLAG(&_insts, QGSDI_DROP, FALSE);
					DragAcceptFiles(_hwnd, FALSE);
				}
			}
		}

		//
		virtual kvoid SetAcsEnable(kbool v)
		{
			if (v)
			{
				if (!K_OFLAG(_insts, QGSDI_ACS))
				{
					STICKYKEYS stk;
					TOGGLEKEYS tgk;
					FILTERKEYS ftk;

					_acs_stk.cbSize = sizeof(STICKYKEYS);
					_acs_tgk.cbSize = sizeof(TOGGLEKEYS);
					_acs_ftk.cbSize = sizeof(FILTERKEYS);
					SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(STICKYKEYS), &_acs_stk, 0);
					SystemParametersInfo(SPI_GETTOGGLEKEYS, sizeof(TOGGLEKEYS), &_acs_tgk, 0);
					SystemParametersInfo(SPI_GETFILTERKEYS, sizeof(FILTERKEYS), &_acs_ftk, 0);

					stk = _acs_stk;
					tgk = _acs_tgk;
					ftk = _acs_ftk;
					stk.dwFlags &= ~(SKF_HOTKEYACTIVE | SKF_CONFIRMHOTKEY);
					tgk.dwFlags &= ~(TKF_HOTKEYACTIVE | TKF_CONFIRMHOTKEY);
					ftk.dwFlags &= ~(FKF_HOTKEYACTIVE | FKF_CONFIRMHOTKEY);
					SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &stk, 0);
					SystemParametersInfo(SPI_SETTOGGLEKEYS, sizeof(TOGGLEKEYS), &tgk, 0);
					SystemParametersInfo(SPI_SETFILTERKEYS, sizeof(FILTERKEYS), &ftk, 0);

#ifdef USE_KEY_HOOK
					_windows::impl_hook.HookKey(this, _hinstance);
#endif

					K_UFLAG(&_insts, QGSDI_ACS, TRUE);
				}
			}
			else
			{
				if (K_OFLAG(_insts, QGSDI_ACS))
				{
					SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &_acs_stk, 0);
					SystemParametersInfo(SPI_SETTOGGLEKEYS, sizeof(TOGGLEKEYS), &_acs_tgk, 0);
					SystemParametersInfo(SPI_SETFILTERKEYS, sizeof(FILTERKEYS), &_acs_ftk, 0);

#ifdef USE_KEY_HOOK
					_windows::impl_hook.UnhookKey(this);
#endif

					K_UFLAG(&_insts, QGSDI_ACS, FALSE);
				}
			}
		}

		//
		virtual kvoid SetImeEnable(kbool v)
		{
			if (!_windows::impl_imm.Initialize())
				return;

			if (v)
			{
				if (k_atomdec(&_imcs) <= 0)
				{
					// 마지막 1번만 함
					k_atomset(&_imcs, 0);

					if (_himc)
					{
						_windows::impl_imm.funcImmAssociateContext(_hwnd, _himc);
						_himc = NULL;
					}
				}
			}
			else
			{
				if (k_atominc(&_imcs) == 1)
				{
					// 최초 1번만 함
					if (!_himc)
						_himc = _windows::impl_imm.funcImmAssociateContext(_hwnd, NULL);
				}
			}
		}

		//
		virtual kbool SetTitle(const char* text)
		{
			k_return_value_if_fail(text, false);

			if (*text)
			{
				_btitle = text;

				kwchar wz[260];
				k_conv_utf8_to_utf16(wz, 260 - 1, _btitle.c_str(), 0);
				SetWindowText(_hwnd, wz);
			}
			else
			{
				_btitle = "";

				SetWindowText(_hwnd, L"");
			}

			return true;
		}

		//
		virtual kvoid HoldMouse(kbool v)
		{
			if (v)
			{
				if (!_mhold)
				{
					SetCapture(_hwnd);
					_mhold = true;
				}
			}
			else
			{
				if (_mhold)
				{
					ReleaseCapture();
					_mhold = false;
				}
			}
		}

		//
		static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT mesg, WPARAM wp, LPARAM lp)
		{
			WindowsStub* self = reinterpret_cast<WindowsStub*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));

			return self ? self->WndProc(hwnd, mesg, wp, lp) : DefWindowProc(hwnd, mesg, wp, lp);
		}

		//
		LRESULT WndProc(HWND hwnd, UINT mesg, WPARAM wp, LPARAM lp)
		{
			kbool b;

			// 시스템 프로시저
			if (!WorkProcSystem((kint)mesg, wp, lp))
				return 0;

			// 마우스
			while (true)
			{
				if (mesg == WM_MOUSEMOVE)
				{
					if (!WorkProcMouseMove(lp))
						return 0;
				}
				else if ((mesg >= WM_LBUTTONDOWN && mesg <= WM_MBUTTONDBLCLK) || (mesg >= WM_XBUTTONDOWN && mesg <= WM_XBUTTONDBLCLK))
				{
					switch (mesg)
					{
						case WM_LBUTTONDOWN:
							b = WorkProcMouseDown(wp, lp, QIM_LEFT);
							break;

						case WM_RBUTTONDOWN:
							b = WorkProcMouseDown(wp, lp, QIM_RIGHT);
							break;

						case WM_MBUTTONDOWN:
							b = WorkProcMouseDown(wp, lp, QIM_MIDDLE);
							break;

						case WM_XBUTTONDOWN:
							b = WorkProcMouseDown(wp, lp, HIWORD(wp) == XBUTTON1 ? QIM_X1 : QIM_X2);
							break;

						case WM_LBUTTONUP:
							b = WorkProcMouseUp(wp, lp, QIM_LEFT);
							break;

						case WM_RBUTTONUP:
							b = WorkProcMouseUp(wp, lp, QIM_RIGHT);
							break;

						case WM_MBUTTONUP:
							b = WorkProcMouseUp(wp, lp, QIM_MIDDLE);
							break;

						case WM_XBUTTONUP:
							b = WorkProcMouseUp(wp, lp, HIWORD(wp) == XBUTTON1 ? QIM_X1 : QIM_X2);
							break;

						default:
							b = true;
							break;
					}

					if (!b)
						return 0;
				}
				else if (mesg == WM_MOUSEWHEEL)
				{
					if (!WorkProcMouseWheel(wp, lp))
						return 0;
				}
				else break;

				goto pos_exit;
			}

			// 키보드
			while (true)
			{
				if (mesg == WM_KEYDOWN)
				{
					if (!WorkProcKeyDown(wp, lp))
						return 0;
				}
				else if (mesg == WM_KEYUP)
				{
					if (!WorkProcKeyUp(wp, lp))
						return 0;
				}
				else if (mesg == WM_SYSKEYDOWN)
				{
					if (!WorkProcKeySysDown(wp, lp))
						return 0;
				}
				else if (mesg == WM_SYSKEYUP)
				{
					if (!WorkProcKeyUp(wp, lp))
						return 0;
				}
				else break;

				goto pos_exit;
			}

			// 기타
			switch (mesg)
			{
				case WM_ERASEBKGND:
					return 0;

				case WM_PAINT:
					break;

				case WM_SETCURSOR:
					if (!WorkProcCursor(lp))
						return 0;
					break;

				case WM_ACTIVATEAPP:
					if (!WorkProcActive(wp))
						return 0;
					break;

				case WM_SIZE:
					if (wp == SIZE_MAXIMIZED)
						WorkProcLayout(true, true);
					else if (wp == SIZE_RESTORED)
					{
						if (!K_OFLAG(_insts, QGSDI_LAYOUT))
							WorkProcLayout(true);
					}
					break;

				case WM_ENTERSIZEMOVE:
					K_UFLAG(&_insts, QGSDI_LAYOUT, true);
					break;

				case WM_EXITSIZEMOVE:
					if (K_OFLAG(_insts, QGSDI_LAYOUT))
					{
						WorkProcLayout();
						K_UFLAG(&_insts, QGSDI_LAYOUT, false);
					}
					break;

				case WM_GETMINMAXINFO:
					((MINMAXINFO*)lp)->ptMinTrackSize.x = _limit.x;
					((MINMAXINFO*)lp)->ptMinTrackSize.y = _limit.y;
					break;

				case WM_ENTERMENULOOP:
				case WM_EXITMENULOOP:
					break;

				case WM_NCHITTEST:
					if ((_flags & QGSDF_FULL) != 0)
						return HTCLIENT;
					break;

				case WM_SYSCOMMAND:
					if (wp == SC_MONITORPOWER)
					{
					}
					else if (wp == SC_SCREENSAVE)
					{
						if (!K_OFLAG(_insts, QGSDI_SCRSAVE))
							return 0;
					}
					break;

				case WM_POWERBROADCAST:
					if (!K_OFLAG(_insts, QGSDI_SCRSAVE))
					{
						if (wp == PBT_APMQUERYSUSPEND)
							return BROADCAST_QUERY_DENY;
						else if (wp == PBT_APMRESUMESUSPEND)
							return 1;
					}
					break;

				case WM_IME_SETCONTEXT:
					lp = 0;
					break;

				case WM_MENUCHAR:
					return MAKELRESULT(0, MNC_CLOSE);

				case WM_CHAR:
					if (!WorkProcChar(wp, lp))
						return 0;
					break;

				case WM_DROPFILES:
					if (!WorkProcDropFile(wp))
						return 0;
					break;

				case WM_CLOSE:
					K_UFLAG(&_insts, QGSDI_EXIT, true);
					DestroyWindow(hwnd);
					_hwnd = NULL;
					return 0;

				case WM_DESTROY:
					PostQuitMessage(0);
					break;
			}

pos_exit:
			return DefWindowProc(hwnd, mesg, wp, lp);
		}

		kbool WorkProcSystem(kint mesg, WPARAM wp, LPARAM lp)
		{
			if (!_sehs.empty())
			{
				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnSystem(_hwnd, wp, lp, mesg))
						return false;
				}
			}

			return true;
		}

		void WorkProcMouseButtonState(WPARAM wp)
		{
			kuint mask = LOWORD(wp);
			kushort state = 0;

			if (mask & MK_LBUTTON)    state |= QIMM_LEFT;
			if (mask & MK_RBUTTON)    state |= QIMM_RIGHT;
			if (mask & MK_MBUTTON)    state |= QIMM_MIDDLE;
			if (mask & MK_XBUTTON1)   state |= QIMM_X1;
			if (mask & MK_XBUTTON2)   state |= QIMM_X2;

			_mouse.state = state;
		}

		void WorkProcMousePoint(LPARAM lp, kbool conv, bool save)
		{
			tpl::Point pt(GET_X_LPARAM(lp), GET_Y_LPARAM(lp));

			if (conv)
				ScreenToClient(_hwnd, (LPPOINT)&pt);

			if (save)
				_mouse.last = _mouse.point;

			_mouse.point = pt;
		}

		bool WorkProcMouseMove(LPARAM lp)
		{
			WorkProcMousePoint(lp, false, true);

			_ProcessMultiClick(0, QIMT_MOVE);

			if (!_sehs.empty())
			{
				kushort s = _mouse.state;
				kint x = _mouse.point.x;
				kint y = _mouse.point.y;
				tpl::Point r(x - _mouse.last.x, y - _mouse.last.y);

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnMouseMove(x, y, r, s))
						return false;
				}
			}

			return true;
		}

		bool WorkProcMouseDown(WPARAM wp, LPARAM lp, kint btn)
		{
			WindowsStub::HoldMouse(true);

			WorkProcMouseButtonState(wp);
			WorkProcMousePoint(lp, false, false);

			bool okdbl = _ProcessMultiClick(btn, QIMT_DOWN);

			// 보내기
			if (_sehs.empty())
				return true;
			else
			{
				bool ret = true;
				kint x = _mouse.point.x;
				kint y = _mouse.point.y;

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnMouseDown(x, y, (kushort)btn))
					{
						ret = false;
						break;
					}
				}

				if (okdbl)
				{
					kForEach(qbSevHandler* p, _sehs)
					{
						if (!p->OnMouseDouble(x, y, (kushort)btn))
						{
							ret = false;
							break;
						}
					}
				}

				return ret;
			}
		}

		bool WorkProcMouseUp(WPARAM wp, LPARAM lp, kint btn)
		{
			WindowsStub::HoldMouse(false);

			WorkProcMouseButtonState(wp);
			WorkProcMousePoint(lp, false, false);

			_ProcessMultiClick(btn, QIMT_UP);

			if (!_sehs.empty())
			{
				kint x = _mouse.point.x;
				kint y = _mouse.point.y;

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnMouseUp(x, y, (kushort)btn))
						return false;
				}
			}

			return true;
		}

		bool WorkProcMouseWheel(WPARAM wp, LPARAM lp)
		{
			WorkProcMousePoint(lp, true, false);

			_mouse.wheel = GET_WHEEL_DELTA_WPARAM(wp);

			if (!_sehs.empty())
			{
				kshort d = _mouse.wheel > 0 ? 1 : -1;

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnMouseWheel(d))
						return false;
				}
			}

			return true;
		}

		bool WorkProcKeyProcDown(kbyte key, WPARAM wp, LPARAM lp)
		{
			kbyte sub = 0;
			bool repeat = (lp & 0x40000000) != 0;
			bool ret;

			// 특수키 상태 나눔
			switch (key)
			{
				case VK_SHIFT:
					K_UFLAG(&_key.state, QIS_SHIFT, true);
					sub = QIK_SHIFT;
					key = (kbyte)MapVirtualKey((lp & 0x00ff0000) >> 16, MAPVK_VSC_TO_VK_EX);
					repeat = _key.key[key] != 0;
					break;

				case VK_CONTROL:
					K_UFLAG(&_key.state, QIS_CTRL, true);
					sub = QIK_CTRL;
					if ((lp & 0x1000000) == 0)
					{
						key = QIK_LCTRL;
						repeat = _key.key[QIK_LCTRL] != 0;
					}
					else
					{
						key = QIK_RCTRL;
						repeat = _key.key[QIK_RCTRL] != 0;
					}
					break;

				case VK_MENU:
					K_UFLAG(&_key.state, QIS_ALT, true);
					sub = QIK_ALT;
					if ((lp & 0x1000000) == 0)
					{
						key = QIK_LALT;
						repeat = _key.key[QIK_LALT] != 0;
					}
					else
					{
						key = QIK_RALT;
						repeat = _key.key[QIK_RALT] != 0;
					}
					break;

				case VK_CAPITAL:	// 토글이다
					ret = !K_OFLAG(_key.state, QIS_CAPS);
					K_UFLAG(&_key.state, QIS_CAPS, ret);
					break;

				case VK_NUMLOCK:	// 토글임
					ret = !K_OFLAG(_key.state, QIS_NUM);
					K_UFLAG(&_key.state, QIS_NUM, ret);
					break;

				case VK_SCROLL:		// 이것도 토클
					ret = !K_OFLAG(_key.state, QIS_SCRL);
					K_UFLAG(&_key.state, QIS_SCRL, ret);
					break;
			}

			//
			ret = true;

			if (!_sehs.empty())
			{
				kuint ch = MapVirtualKey(wp, MAPVK_VK_TO_CHAR);
				kushort s = _key.state;

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnKeyDown(key, sub, ch, repeat, s))
					{
						ret = false;
						break;
					}
				}
			}

			_key.key[key] = true;
			_key.key[sub] = true;

			return ret;
		}

		bool WorkProcKeyDown(WPARAM wp, LPARAM lp)
		{
			kbyte key = (kbyte)(wp & 0xFF);

#if _SB_DEBUG_
			if (key == VK_PAUSE)
			{
				SendMessage(_hwnd, WM_CLOSE, 0, 0);
				return true;
			}
#endif

			return WorkProcKeyProcDown(key, wp, lp);
		}

		bool WorkProcKeySysDown(WPARAM wp, LPARAM lp)
		{
			kbyte key = (kbyte)(wp & 0xFF);

			return !WorkProcKeyProcDown(key, wp, lp) ? false : key != VK_MENU;
		}

		bool WorkProcKeyUp(WPARAM wp, LPARAM lp)
		{
			kbyte key = (kbyte)(wp & 0xFF);
			kbyte sub = 0;
			kbyte tst = 0;

			// 특수키 상태
			switch (key)
			{
				case VK_SHIFT:
					sub = QIK_SHIFT;
					key = (kbyte)MapVirtualKey((lp & 0x00ff0000) >> 16, MAPVK_VSC_TO_VK_EX);
					if (key == QIK_LSHIFT)
					{
						key = QIK_LSHIFT;
						if (!_key.key[QIK_RSHIFT])
						{
							K_UFLAG(&_key.state, QIS_SHIFT, false);
							tst = QIK_SHIFT;
						}
					}
					else
					{
						key = QIK_RSHIFT;
						if (!_key.key[QIK_LSHIFT])
						{
							K_UFLAG(&_key.state, QIS_SHIFT, false);
							tst = QIK_SHIFT;
						}
					}
					break;

				case VK_CONTROL:
					sub = QIK_CTRL;
					if ((lp & 0x1000000) == 0)
					{
						key = QIK_LCTRL;
						if (!_key.key[QIK_RCTRL])
						{
							K_UFLAG(&_key.state, QIS_CTRL, false);
							tst = QIK_CTRL;
						}
					}
					else
					{
						key = QIK_RCTRL;
						if (!_key.key[QIK_RCTRL])
						{
							K_UFLAG(&_key.state, QIS_CTRL, false);
							tst = QIK_CTRL;
						}
					}
					break;

				case VK_MENU:
					sub = QIK_ALT;
					if ((lp & 0x1000000) == 0)
					{
						key = QIK_LALT;
						if (!_key.key[QIK_RALT])
						{
							K_UFLAG(&_key.state, QIS_ALT, false);
							tst = QIK_ALT;
						}
					}
					else
					{
						key = QIK_RALT;
						if (!_key.key[QIK_LALT])
						{
							K_UFLAG(&_key.state, QIS_ALT, false);
							tst = QIK_ALT;
						}
					}
					break;
			}

			// 보내기
			bool ret = true;

			if (!_sehs.empty())
			{
				kushort s = _key.state;

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnKeyUp(key, sub, s))
					{
						ret = false;
						break;
					}
				}
			}

			_key.key[key] = false;
			_key.key[tst] = false;

			return ret;
		}

		bool WorkProcChar(WPARAM wp, LPARAM lp)
		{
			if (!_sehs.empty())
			{
				kuint ch = (kuint)wp;
				bool repeat = (lp & 0x40000000) != 0;

				kForEach(qbSevHandler* p, _sehs)
				{
					if (!p->OnChar(ch, repeat))
						return false;
				}
			}

			return true;
		}

		bool WorkProcActive(WPARAM wp)
		{
			// WM_ACTIVATE -> LOWORD(wp) != WA_INACTIVE

			double now = k_timer_get_abs(_timer);
			float adt = (float)(now - _active);

			_active = now;

			if ((BOOL)wp)
			{
				K_UFLAG(&_insts, QGSDI_ACTIVE, true);

				if (!_sehs.empty())
				{
					kForEach(qbSevHandler* p, _sehs)
					{
						if (!p->OnActivate(true, adt))
							return false;
					}
				}
			}
			else
			{
				K_UFLAG(&_insts, QGSDI_ACTIVE, false);

				if (!_sehs.empty())
				{
					bool isok = true;

					kForEach(qbSevHandler* p, _sehs)
					{
						if (!p->OnActivate(false, adt))
						{
							isok = false;
							break;
						}
					}

					kushort s = _key.state;

					for (kint i = 0; i < 256; i++)
					{
						if (!_key.key[i])
							continue;

						_key.key[i] = false;

						kForEach(qbSevHandler* p, _sehs)
							p->OnKeyUp((kbyte)i, 0, s);
					}

					if (!isok)
						return false;
				}
			}

			return true;
		}

		void WorkProcLayout(bool force = false, bool ismax = false)
		{
			tpl::Point prev(_size);

			GetWindowRect(_hwnd, (LPRECT)&_wndbnd);
			GetClientRect(_hwnd, (LPRECT)&_bound);

			if (!K_OFLAG(_flags, QGSDF_FULL))
				_size.Set(_bound.Width(), _bound.Height());

			// 이벤트 트리거
			if (qgRdh::Instance != NULL)
				qgRdh::Instance->Layout(_size.width, _size.height);

			if ((force || prev.x != _size.x || prev.y != _size.y) &&
				!_sehs.empty())
			{
				kForEach(qbSevHandler* p, _sehs)
					p->OnLayout(_size.width, _size.height, false);
			}
		}

		bool WorkProcDropFile(WPARAM wp)
		{
			if (!K_OFLAG(_insts, QGSDI_DROP) || _sehs.empty())
				return true;

			kint cnt = (kint)DragQueryFile((HDROP)wp, 0xFFFFFFFF, NULL, 0);
			k_return_value_if_fail(cnt > 0, true);

			DragAcceptFiles(_hwnd, false);

			tpl::vector<tpl::string> ss(cnt);

			for (kint i = 0; i < cnt; i++)
			{
				kint len = (kint)DragQueryFile((HDROP)wp, i, NULL, 0);

				if (len > 0)
				{
					kwchar wz[260];
					char sz[260];

					DragQueryFile((HDROP)wp, i, wz, 260 - 1);
					len = (kint)k_conv_utf16_to_utf8(sz, 260 - 1, wz, len);

					if (len > 0)
						ss.push_back(tpl::string(sz));
				}
			}

			//
			bool ret = true;

			kForEach(qbSevHandler* p, _sehs)
			{
				if (!p->OnDragDrop(ss))
				{
					ret = false;
					break;
				}
			}

			DragAcceptFiles(_hwnd, true);

			return ret;
		}

		bool WorkProcCursor(LPARAM lp)
		{
			if (K_OFLAG(_insts, QGSDI_CURSOR))
			{
				if (_sehs.empty())
				{
					SetCursor(LoadCursor(NULL, IDC_ARROW));

					return true;
				}
				else
				{
					kForEach(qbSevHandler* p, _sehs)
					{
						if (!p->OnCursor())
							return false;
					}

					return true;
				}
			}
			else
			{
				SetCursor(NULL);

				return false;
			}
		}
	};
}


//////////////////////////////////////////////////////////////////////////
// 만들기
namespace _qgp
{
	qgStub* NewStub(const tpl::Rect& area, kint flags, const char* title, kpointer)
	{
		_windows::WindowsStub* stub = kobj_create(_windows::WindowsStub);

		if (!stub->Create(area, flags, title))
		{
			stub->Unload();
			stub = NULL;
		}

		return stub;
	}

	qgStub* NewStubVirtual(kpointer handle)
	{
		_windows::WindowsStub* stub = kobj_create(_windows::WindowsStub);

		if (!stub->CreateVirtual(handle))
		{
			stub->Unload();
			stub = NULL;
		}

		return stub;
	}
}

#endif
